using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;

namespace Radiality {
	/// <summary>
	/// A somewhat trivial IoC container with minimal reflection, based on an idea from 
	/// http://www.ayende.com/Blog/archive/2007/10/20/Building-an-container-container-in-15-lines-of-code.aspx
	/// 
	/// I could be described both as a IoC type 1 and type 2 (Not to self: I should a probably have an 
	/// extended description in my blog
	/// All the configuration is in code for now, could easily be extended to build 
	/// the container from attributes or XML. By default bindings are lazy and not evaluated until needed, 
	/// if an eager binding is needed use AsEagerSingleton
	/// </summary>
	/// 
	/// <remarks>
	/// A container (and its registered components) can get access to components registered in a parent container.
	/// The problem with setter injection is that you can end with incomplete objects but this is the best I can do
	/// since construction injection is not available.
	/// 
	/// Also since some XNA objects can only be created a certain times (and by this I mean during LoadContent) 
	/// some properties need to be updated after the object was created, this is where InjectProperty is useful.
	/// 
	/// The autowiring strategy is applied at the object level, going to property level seems overkill.
	/// </remarks>
	/// 
	/// <example> Simple usage
	/// <code>
	/// RadialityContainer container = new RadialityContainer();
	/// container.Bind<SomeClass>().to(delegate {return new SomeClass();}).AsEagerSingleton();
	/// SomeClass sc = container.Get<SomeClass>();
	/// </code>
	/// </example>
	public class RadialityContainer {
		private string name;
		public string Name {
			get { return name; }
			set { name = value; }
		}

		private readonly RadialityContainer parent;

		private readonly Dictionary<object, object> singletonScopedInstances =
			new Dictionary<object, object>();
		private readonly List<WeakReference> prototypeScopedInstanceReferences =
			new List<WeakReference>();
		private const int prototypeScopedReferenceTreshold = 128;
		private readonly Dictionary<object, Binding> bindings =
			new Dictionary<object, Binding>();

		public delegate object BindingProvider(RadialityContainer container);

		private int prototypeScopedInstancesCreated;
		public int PrototypeScopedInstancesCreated {
			get { return prototypeScopedInstancesCreated; }
		}

		public int SingletonsCount {
			get { return singletonScopedInstances.Count; }
		}

		public event EventHandler<InstanceCreatedEventArgs> InstanceCreated;

		public class InstanceCreatedEventArgs : EventArgs {
			public object instance;
			public Scope scope;

			public InstanceCreatedEventArgs(object instance, Scope scope) {
				this.instance = instance;
				this.scope = scope;
			}
		}

		/// <summary>
		/// This delegate is used to obtain the key that will be used to retrieved the binding from the
		/// container. Default strategies for name and type are provided.
		/// </summary>
		/// <param name="property"></param>
		/// <returns>the binding key</returns>
		public delegate object AutowiringStrategy(PropertyInfo property);

		public static AutowiringStrategy AutowireByNameStrategy =
			property => property.Name;

		public static AutowiringStrategy AutowireByTypeStrategy =
			property => property.PropertyType;

		private List<AutowiringStrategy> defaultPropertyAutowiringStrategies =
			new List<AutowiringStrategy>();
		public List<AutowiringStrategy> DefaultPropertyAutowiringStrategies {
			get { return defaultPropertyAutowiringStrategies; }
			set { defaultPropertyAutowiringStrategies = value; }
		}

		public enum Scope {
			Singleton,
			EagerSingleton,
			Prototype
		}

		private Scope defaultScope = Scope.Singleton;
		public Scope DefaultScope {
			get { return defaultScope; }
			set { defaultScope = value; }
		}

		#region Binding class
		public class Binding {
			public List<AutowiringStrategy> AutowiringStrategies { get; set; }
			internal Object key;
			internal BindingProvider provider;
			internal RadialityContainer container;
			internal List<AutowiringStrategy> autowiringStrategies =
				new List<AutowiringStrategy>();
			internal Scope scope = Scope.Singleton;

			public Binding(Object key, BindingProvider provider, Scope scope,
										 List<AutowiringStrategy> autowiringStrategies,
										 RadialityContainer container) {
				AutowiringStrategies = autowiringStrategies;
				this.key = key;
				this.provider = provider;
				this.scope = scope;
				this.autowiringStrategies = autowiringStrategies;
				this.container = container;
			}

			public Binding To(BindingProvider p) {
				provider = p;
				return this;
			}

			public Binding AutowireProperties(AutowiringStrategy autowiringStrategy) {
				autowiringStrategies.Clear();
				autowiringStrategies.Add(autowiringStrategy);
				return this;
			}

			public Binding AutowirePropertiesByName() {
				autowiringStrategies.Clear();
				autowiringStrategies.Add(AutowireByNameStrategy);
				return this;
			}

			public Binding AutowirePropertiesByType() {
				autowiringStrategies.Clear();
				autowiringStrategies.Add(AutowireByTypeStrategy);
				return this;
			}

			public Binding NoAutowiring() {
				autowiringStrategies.Clear();
				return this;
			}

			public RadialityContainer AsSingleton() {
				scope = Scope.Singleton;
				return container;
			}

			public RadialityContainer AsEagerSingleton() {
				Debug.WriteLine(
					"Eagerly binding " + key + " to " + provider + " as " + scope);
				container.FindOrInstantiateSingleton<object>(this);
				return container;
			}

			public RadialityContainer AsPrototype() {
				scope = Scope.Prototype;
				return container;
			}

			public RadialityContainer InjectNow() {
				if (scope != Scope.Singleton)
					throw new Exception("Only singleton \"injection on demand\" is supported");
				container.InjectBinding(this);
				return container;
			}
		}
		#endregion

		#region Constructors
		public RadialityContainer()
			: this(null) {
		}

		public RadialityContainer(RadialityContainer parent) {
			this.parent = parent;
			defaultPropertyAutowiringStrategies.Add(AutowireByNameStrategy);
			defaultPropertyAutowiringStrategies.Add(AutowireByTypeStrategy);
		}

		public RadialityContainer Merge(RadialityContainer fragment) {
			return Merge(fragment, false, false);
		}

		public RadialityContainer Merge(RadialityContainer fragment,
																		bool replaceExistingBindings,
																		bool replaceExistingSingletons) {
			Debug.WriteLine("Merging " + this + " with " + fragment);
			foreach (var key in fragment.bindings.Keys) {
				if (bindings.ContainsKey(key) && !replaceExistingBindings)
					continue;
				bindings.Add(key, fragment.bindings[key]);
			}
			foreach (var key in fragment.singletonScopedInstances.Keys) {
				if (singletonScopedInstances.ContainsKey(key) && !replaceExistingSingletons)
					continue;
				singletonScopedInstances.Add(key, fragment.singletonScopedInstances[key]);
			}
			return this;
		}
		#endregion

		#region Private methods for binding creation and manipulation
		private Binding AddBinding(Binding binding) {
			Debug.WriteLine(
				"Binding " + binding.key + " to " + binding.provider + " as " +
				binding.scope);
			bindings.Add(binding.key, binding);
			if (binding.scope == Scope.EagerSingleton) {
				binding.AsEagerSingleton();
			}
			return binding;
		}

		public T AutowireByName<T>(T o) {
			return AutowireProperties(o, AutowireByNameStrategy);
		}

		public T AutowireByType<T>(T o) {
			return AutowireProperties(o, AutowireByTypeStrategy);
		}

		/// <summary>
		/// Goes through all the properties of the object and tries to inject the appropriate 
		/// values from the container based on the autowiring strategy of the binding.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="o"></param>
		/// <param name="autowiringStrategy"></param>
		/// <returns></returns>
		public T AutowireProperties<T>(T o, AutowiringStrategy autowiringStrategy) {
			var properties =
				o.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
			foreach (var property in properties) {
				try {
					var value = Get<object>(autowiringStrategy(property));
					InjectProperty(o, property, value);
				} catch (ArgumentException) {
					//ignore if a property binding is not found
				}
			}
			return o;
		}

		private static void InjectProperty<T>(T o, PropertyInfo property, object value) {
			if (!property.CanWrite) return;
			Debug.WriteLine("Injecting " + value + " into " + property.ReflectedType + "." +
				property.Name);
			property.SetValue(o, value, null);
		}

		private bool FindBinding(object key, out Binding binding) {
			if (bindings.TryGetValue(key, out binding)) {
				return true;
			}
			if (parent != null) {
				Debug.WriteLine("Binding not found locally, searching parent " + parent);
				return parent.FindBinding(key, out binding);
			}
			return false;
		}

		private T FindOrInstantiateSingleton<T>(Binding binding) {
			Object singleton;
			if (singletonScopedInstances.TryGetValue(binding.key, out singleton)) {
				return (T)singleton;
			}

			if (!bindings.ContainsValue(binding)) {
				//this that the binding was found in the parent
				return parent.FindOrInstantiateSingleton<T>(binding);
			}

			Debug.WriteLine("Creating singleton for [" + binding.key + "]");
			Debug.IndentLevel++;

			singleton = binding.provider(this);
			singletonScopedInstances.Add(binding.key, singleton);

			foreach (var propertyAutowiringStrategy in binding.autowiringStrategies)
				singleton = AutowireProperties((T)singleton, propertyAutowiringStrategy);

			var handler = InstanceCreated;
			if (handler != null) {
				handler(this, new InstanceCreatedEventArgs(singleton, binding.scope));
			}
			Debug.IndentLevel--;
			return (T)singleton;
		}

		private T InstantiatePrototype<T>(Binding binding) {
			Debug.IndentLevel++;
			var instance = (T)binding.provider(this);
			foreach (var propertyAutowiringStrategy in binding.autowiringStrategies)
				instance = AutowireProperties(instance, propertyAutowiringStrategy);

			prototypeScopedInstanceReferences.Add(new WeakReference(instance));
			if (prototypeScopedInstancesCreated % prototypeScopedReferenceTreshold == 0) {
				CleanupPrototypeReferences();
			}
			prototypeScopedInstancesCreated++;
			var handler = InstanceCreated;
			if (handler != null) {
				handler(this, new InstanceCreatedEventArgs(instance, binding.scope));
			}
			Debug.IndentLevel--;
			return instance;
		}

		private void CleanupPrototypeReferences() {
			for (var index = prototypeScopedInstanceReferences.Count; index > 0; index--) {
				var wr = prototypeScopedInstanceReferences[index - 1];
				if (!wr.IsAlive) {
					prototypeScopedInstanceReferences.Remove(wr);
				}
			}
		}
		#endregion

		#region Bind methods
		public Binding Bind(object key, BindingProvider provider, Scope mode) {
			var binding = AddBinding(new Binding(key, provider, mode, defaultPropertyAutowiringStrategies, this));
			return binding;
		}

		public Binding Bind(object key, BindingProvider provider) {
			return Bind(key, provider, defaultScope);
		}

		public Binding Bind<T>(BindingProvider provider, Scope mode) {
			return Bind(typeof(T), provider, mode);
		}

		public Binding Bind<T>(BindingProvider provider) {
			return Bind<T>(provider, defaultScope);
		}

		public Binding Bind(object key) {
			return Bind(key, null);
		}

		public Binding Bind<T>() {
			return Bind(typeof(T));
		}

		/// <summary>
		/// Inject this binding in all the objects, both singletons or prototype scoped
		/// </summary>
		public Binding InjectBinding(Binding bindingToInject) {
			//inject singletons
			var keys = new object[singletonScopedInstances.Count];
			singletonScopedInstances.Keys.CopyTo(keys, 0);
			//foreach(object k in singletonScopedInstances.Keys) {
			for (var i = 0; i < keys.Length; i++) {
				var k = keys[i];
				Binding binding;
				if (!bindings.TryGetValue(k, out binding))
					continue;
				if (binding.key == bindingToInject.key)
					continue;
				var o = singletonScopedInstances[k];
				var properties = o.GetType().GetProperties();
				foreach (var property in properties) {
					InjectObject(o, property, binding, bindingToInject);
				}
			}

			//inject prototypes
			foreach (var wr in prototypeScopedInstanceReferences) {
				if (!wr.IsAlive)
					continue;
				var o = wr.Target;
				var binding = bindings[o];
				var properties = o.GetType().GetProperties();
				foreach (var property in properties) {
					InjectObject(o, property, binding, bindingToInject);
				}
			}
			return bindingToInject;
		}

		private void InjectObject(Object o, PropertyInfo property, Binding binding,
															Binding bindingToInject) {
			var value = Get<object>(bindingToInject.key);
			foreach (var strategy in binding.autowiringStrategies) {
				if (strategy == AutowireByNameStrategy &&
						property.Name.Equals(bindingToInject.key)) {
					InjectProperty(o, property, value);
					return;
				}
				if (strategy != AutowireByTypeStrategy || !(bindingToInject.key is Type) ||
						!property.PropertyType.IsAssignableFrom(value.GetType())) {
					continue;
				}
				InjectProperty(o, property, value);
			}
		}
		#endregion

		#region Binding access
		public T Get<T>() {
			return Get<T>(typeof(T));
		}

		public T Get<T>(object key) {
			Binding binding;
			if (!FindBinding(key, out binding)) {
				throw new ArgumentException(
					"Key [" + key + "] not bound. Check you container configuration.");
			}
			switch (binding.scope) {
				case Scope.Singleton:
					return FindOrInstantiateSingleton<T>(binding);
				case Scope.Prototype:
					return InstantiatePrototype<T>(binding);
				default:
					throw new ArgumentException("Unknown mode " + binding.scope);
			}
		}

		/// <summary>
		/// Execute the delegate bound to the given key. Useful for things like lifecycle 
		/// management.
		/// </summary>
		/// <param name="key"></param>
		public void Execute(object key) {
			Debug.WriteLine("Execute [" + key + "]");
			Debug.IndentLevel++;
			Binding binding;
			if (bindings.TryGetValue(key, out binding)) {
				binding.provider(this);
			}
			Debug.IndentLevel--;
			Debug.WriteLine("Execute [" + key + "] completed");
		}
		#endregion

		public override string ToString() {
			return name ?? "container[" + base.GetHashCode() + "]";
		}
	}
}